package cn.yiynx.example.util.excel;

import cn.hutool.core.lang.TypeReference;
import cn.yiynx.example.util.limiter.SlidingWindow;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.Date;
import java.util.concurrent.ExecutionException;
import java.util.function.UnaryOperator;

@Slf4j
public class XExcelUtil {
    public static final Integer EXCEL_SHEET_ROW_MAX_SIZE = 1000001; // excel sheet最大行数(算标题)
    private static final long DEF_PAGE_SIZE = 1000; // 默认页大小
    private static final int DEF_PARALLEL_NUM = Math.min(Runtime.getRuntime().availableProcessors(), 3);
    private HttpServletResponse httpServletResponse;
    private boolean parallel;
    private long pageSize = DEF_PAGE_SIZE;
    private int parallelNum = DEF_PARALLEL_NUM;
    private String fileName;

    private XExcelUtil() {}

    public static XExcelUtil download(HttpServletResponse response, String fileNamePrefix) throws UnsupportedEncodingException {
        XExcelUtil excelUtil = new XExcelUtil();
        excelUtil.httpServletResponse = response;
        excelUtil.fileName = fileNamePrefix + "_" + Date.xNow().xFormat("yyyyMMddHHmmss") + ".xlsx";
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String downloadFileName = URLEncoder.encode(excelUtil.fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + downloadFileName);
        return excelUtil;
    }

    public XExcelUtil parallel() {
        this.parallel = true;
        return this;
    }

    public XExcelUtil parallel(int parallelNum) {
        this.parallel = true;
        this.parallelNum = parallelNum;
        return this;
    }

    public XExcelUtil pageSize(int pageSize) {
        this.pageSize = pageSize;
        return this;
    }

    @SneakyThrows
    public <T> void pageExcelWriter(Class<T> head, UnaryOperator<IPage<T>> pageFunction) {
        if (parallel) {
            pageExcelWriterParallel(httpServletResponse.getOutputStream(), fileName, head, parallelNum, pageSize, pageFunction);
        } else {
            pageExcelWriter(httpServletResponse.getOutputStream(), fileName , head, pageSize, pageFunction);
        }
    }

    private static <T> void pageExcelWriter(OutputStream outputStream, String fileName, Class<T> head, long pageSize, UnaryOperator<IPage<T>> pageFunction) {
        long start = System.currentTimeMillis();
        log.debug("fileName:{}, excel writer start", fileName);
        try (ExcelWriter excelWriter = EasyExcel.write(outputStream, head).build()) {
            IPage<T> page = null;
            WriteSheet writeSheet = EasyExcel.writerSheet(0).build();
            do {
                long pageSearchStartTime = System.currentTimeMillis();
                page = pageFunction.apply(page == null ? new Page<>(1, pageSize) : new Page<>(page.getCurrent() + 1, page.getSize(), page.getTotal(), false)); // 分页查询
                long pageExcelWriteStartTime = System.currentTimeMillis();
                writeSheet.setSheetNo((int) (page.getCurrent() * page.getSize() / EXCEL_SHEET_ROW_MAX_SIZE));
                excelWriter.write(page.getRecords(), writeSheet); // excel写入数据
                log.debug("fileName:{}, total:{}, pageSize:{}, totalPage:{}, pageNo:{}, sheetNo:{}, pageSearchTime:{}ms, pageExcelWriterTime:{}ms", fileName, page.getTotal(), page.getSize(), page.getPages(), page.getCurrent(), writeSheet.getSheetNo(), pageExcelWriteStartTime - pageSearchStartTime, System.currentTimeMillis() - pageExcelWriteStartTime);
            } while (page.getCurrent() < page.getPages()); // 是否还有下一页
        }
        log.debug("fileName:{}, excel writer done, totalTime:{}ms", fileName, System.currentTimeMillis() - start);
    }

    private static <T> void pageExcelWriterParallel(OutputStream outputStream, String fileName, Class<T> head, int parallelNum, long pageSize, UnaryOperator<IPage<T>> pageFunction) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        log.debug("fileName:{}, excel writer start", fileName);
        try (ExcelWriter excelWriter = EasyExcel.write(outputStream, head).build()) {
            final WriteSheet writeSheet = EasyExcel.writerSheet(0, "Sheet0").build();
            IPage<T> basePage = pageFunction.apply(new Page<>(1, 0)).setSize(pageSize); // XXX 查总条数+总页数
            log.debug("fileName:{}, total:{}, pageSize:{}, totalPage:{}", fileName, basePage.getTotal(), basePage.getSize(), basePage.getPages());
            SlidingWindow.create(new TypeReference<IPage<T>>(){}, parallelNum, basePage.getPages()).sendWindow(pageNo -> {
                long pageSearchStartTime = System.currentTimeMillis();
                IPage<T> page = pageFunction.apply(new Page<>(pageNo, basePage.getSize(), basePage.getTotal(), false));
                log.debug("fileName:{}, [读]pageNo:{}, total:{}, pageSize:{}, totalPage:{}, pageSearchTime:{}ms", fileName, page.getCurrent(), page.getTotal(), page.getSize(), page.getPages(), (System.currentTimeMillis() - pageSearchStartTime));
                return page;
            }).receiveWindow(page -> {
                long pageWriteStartTime = System.currentTimeMillis();
                writeSheet.setSheetNo((int) (page.getCurrent() * page.getSize() / EXCEL_SHEET_ROW_MAX_SIZE));
                writeSheet.setSheetName("Sheet" + writeSheet.getSheetNo());
                excelWriter.write(page.getRecords(), writeSheet);
                log.debug("fileName:{}, [写]pageNo:{}, total:{}, pageSize:{}, totalPage:{}, sheetNo:{}, pageWriteTime:{}ms", fileName, page.getCurrent(), page.getTotal(), page.getSize(), page.getPages(), writeSheet.getSheetNo(), (System.currentTimeMillis() - pageWriteStartTime));
            }).start();
        }
        log.debug("fileName:{}, excel writer done, totalTime:{}ms", fileName, System.currentTimeMillis() - start);
    }
}
